# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import ccxt
import httpx
import json
import os.path
import re
import time
import traceback
from bs4 import BeautifulSoup as BS4
from api.position import get_spot_position, bn_transfer_spot_to_u
from api.market import load_market, fetch_binance_ticker_data
from api.trade import place_order, get_twap_symbol_info_list
from utils.functions import get_account_info
from utils.dingding import send_wechat_work_msg
from config import data_path, proxy, exchange_basic_config, account_config, error_webhook_url
import warnings
warnings.filterwarnings('ignore')

# 公告详细的baseurl
baseurl = 'https://www.binance.com/en/support/announcement/'
# 保存下架币种信息的位置
de_list_path = os.path.join(data_path, 'delist.json')
# 监测下架公告时间间隔。默认：60s监测一次，设置太短会被BN封禁
monitor_time = 60
# 获取数据的公共交易所对象
common_exchange = ccxt.binance(exchange_basic_config)

proxies = {'http://': proxy['http'], 'https://': proxy['https']} if proxy else None
headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0'}

def timed_task():
    while True:
        try:
            # 构造请求数据
            params = (('catalogId', '161'), ('pageNo', '1'), ('pageSize', '50'),)
            # 请求公告链接
            response = httpx.get('https://www.binance.com/bapi/composite/v1/public/cms/article/catalog/list/query',
                                 headers=headers,
                                 proxies=proxies,
                                 params=params,
                                 timeout=50)
            if response.status_code == 403:
                print('ip被封了，等待15分钟后再试')
                time.sleep(15*60)
                continue
            # 判断请求是否成功
            if response.json()["success"]:
                data = response.json()["data"]["articles"]
                # ===读取本地下架数据
                try:
                    with open(de_list_path, 'r') as file:
                        de_list_dict = json.load(file)
                except Exception as e:
                    de_list_dict = {'last_id': 0, 'list': []}

                de_list = []
                # 遍历最新的公告数据
                for item in data:
                    # 判断公告id是否超过最新的id
                    if de_list_dict['last_id'] >= item['id']:
                        continue

                    # 判断是否是逐仓保证金交易，直接跳过
                    if 'Isolated Margin' in item['title']:
                        continue

                    # 标题含有 delist 可以直接解析币种信息
                    if 'delist' in item['title'].lower():
                        # 匹配正则规则
                        matches = re.findall(r'\b[A-Z]+\b', item['title'].replace('/', '').replace('USDⓈ-M', ''))
                        print('标题：', matches)
                        de_list.extend(matches)
                    else:  # 标题不含有 delist，需要通过公告内容解析币种信息
                        # 移除非字母数字字符，将空格替换为连字符，并转为小写
                        clean_title = re.sub(r'\W+', ' ', item['title']).strip().replace(' ', '-').lower()
                        # 构建详情地址
                        url = f"{baseurl}{clean_title}-{item['code']}"
                        try:
                            # 打开页面
                            sub_page = httpx.get(url, headers=headers, proxies=proxies, timeout=50)
                            # 解析内容
                            bs = BS4(sub_page.text)
                            ele = bs.select_one('#__APP_DATA')
                            # 转成json格式
                            json_obj = json.loads(ele.text)
                            # 解析需要的内容
                            body = json_obj['appState']['loader']['dataByRouteId']['2a3f']['articleDetail']['body']
                        except err:
                            print(f'url内容解析失败：{url}', err)
                            continue
                        # 匹配正则规则
                        matches = re.findall(r'\b[A-Z]+\b', body.replace('/', '').replace('USDⓈ-M', ''))
                        print('文章解析：', matches)
                        de_list.extend(matches)

                # 数据整理 (这里直接一刀切，有些币种是现货，有些是汇率对，都直接追加USDT，币种名称显示比较奇怪)
                # 这里追加不会有太大问题，我们主要使用USDT交易，所以这是没有问题的
                de_list = [_.replace('USDT', '') + 'USDT' for _ in de_list]  # 去掉USDT，再追加USDT结尾。防止一些公告中币种添加USDT后缀
                de_list = [_.replace('BUSDUSDT', 'BUSD') for _ in de_list]  # 去掉BUSDUSDT，改成BUSD。24年BN将不支持BUSD
                # 筛选出新增的下架币种
                new_data = list(set(de_list) - set(de_list_dict['list']))

                # ===发现新公告币种下架，执行平仓下架币种功能
                if len(new_data) > 0:
                    print(f'下架币种：{new_data}')
                    # 卖掉持仓
                    try:
                        clear_delist_pos(new_data)
                    except BaseException as e:
                        traceback.print_exc()
                        print('清仓失败', e)

                    # 发送下架通知(首次启动会load很多数据，建议首次之后手动打开)
                    # send_wechat_work_msg(f'下架币种：{new_data}', error_webhook_url)

                    # ===更新数据
                    # 更新最新的下架公告id
                    de_list_dict['last_id'] = max([_['id'] for _ in data])
                    # 追加新下架的币种
                    de_list_dict['list'].extend(new_data)

                    # ===保存下架数据
                    with open(de_list_path, 'w') as file:
                        json.dump(de_list_dict, file)
                else:
                    print('没有新公告出现')
        except Exception as e:
            traceback.print_exc()
            print(f'币安下架币种接口报错：{e}')

        print('=' * 20, f'当前循环结束，sleep {monitor_time}s 准备进入下一轮循环', '=' * 20)
        time.sleep(monitor_time)


def clear_delist_pos(delist):
    swap_symbol_list, swap_min_qty, swap_price_precision, swap_min_notional = load_market(common_exchange,
                                                                                          symbol_type='swap')
    # =加载现货交易对信息
    spot_symbol_list, spot_min_qty, spot_price_precision, spot_min_notional = load_market(common_exchange,
                                                                                          symbol_type='spot')
    # =====获取账户信息,并且筛选含有现货的账户
    update_account_info = get_account_info(account_config.copy(), is_only_spot_account=True, is_operate=False)
    # 判断是否没能成功读取任一账户
    if not len(update_account_info.keys()):  # 如果update_account_info数据为空，表示更新账户信息失败
        print(f'delist脚本 所有账户更新信息失败')
        return

        # =====遍历所有账号监测
    for account_name, account_info in update_account_info.items():
        # ===获取账号的配置
        exchange = account_info['exchange']
        spot_position = account_info['spot_position']
        swap_position = account_info['swap_position']
        max_one_order_amount = account_info['max_one_order_amount']
        twap_interval = account_info['twap_interval']
        wechat_webhook_url = account_info['wechat_webhook_url']

        # ===判断当前是否有仓位
        if spot_position.empty and swap_position.empty:  # 当前没有仓位直接跳过
            print('当前没有任何【持仓】，跳过后续操作')
            continue

        # ===计算下单信息
        swap_position = swap_position[swap_position.index.isin(delist)]
        swap_position['实际下单量'] = swap_position['当前持仓量'] * -1  # 直接通过当前持仓进行计算，方向要去反
        swap_position['实际下单资金'] = swap_position['实际下单量'] * swap_position['当前标记价格']  # 计算实际下单资金，用于后续拆单
        swap_position['交易模式'] = '清仓'  # 设置交易模式
        swap_position = swap_position[abs(swap_position['实际下单量']) > 0]  # 保留实际下单量 > 0 的数据
        swap_position.reset_index(inplace=True)
        print('合约下单信息：\n', swap_position)

        spot_position = spot_position[spot_position.index.isin(delist)]
        spot_position['实际下单量'] = spot_position['当前持仓量'] * -1  # 直接通过当前持仓进行计算，方向要去反
        spot_position['实际下单资金'] = spot_position['实际下单量'] * spot_position['当前价格']  # 计算实际下单资金，用于后续拆单
        spot_position['交易模式'] = '清仓'  # 设置交易模式
        spot_position = spot_position[abs(spot_position['实际下单量']) > 0]  # 保留实际下单量 > 0 的数据
        spot_position.reset_index(inplace=True)
        print('现货下单信息：\n', spot_position)

        # 判断是否需要有下单信息
        if swap_position.empty and spot_position.empty:
            continue

        # ===使用twap算法拆分订单
        twap_swap_symbol_info_list = get_twap_symbol_info_list(swap_position, max_one_order_amount)
        # ===遍历下单
        for i in range(len(twap_swap_symbol_info_list)):
            # 获取币种的最新价格
            symbol_last_price = fetch_binance_ticker_data(exchange)
            # 逐批下单(拉黑之后，增加一点滑点下单，防止下单失败)
            place_order(exchange, twap_swap_symbol_info_list[i], symbol_last_price, swap_min_qty,
                        swap_price_precision, swap_min_notional, symbol_type='swap',
                        wechat_webhook_url=wechat_webhook_url, slip_rate=0.03)
            # 下单间隔
            print(f'等待 {twap_interval}s 后继续下单')
            time.sleep(twap_interval)

        # ===现货处理
        if not spot_position.empty:
            # ===使用twap算法拆分订单
            twap_spot_symbol_info_list = get_twap_symbol_info_list(spot_position, max_one_order_amount)
            # ===现货遍历下单
            for i in range(len(twap_spot_symbol_info_list)):
                # 获取现货币种的最新价格
                symbol_last_price = fetch_binance_ticker_data(exchange, symbol_type='spot')
                # 逐批下单(拉黑之后，增加一点滑点下单，防止下单失败)
                place_order(exchange, twap_spot_symbol_info_list[i], symbol_last_price, spot_min_qty,
                            spot_price_precision, spot_min_notional, symbol_type='spot',
                            wechat_webhook_url=wechat_webhook_url, slip_rate=0.03)
                # 下单间隔
                print(f'等待 {twap_interval}s 后继续下单')
                time.sleep(twap_interval)

            # ===将现货中的U转入的合约账号
            spot_position = get_spot_position(exchange)
            if 'USDT' in spot_position['symbol'].to_list():
                usdt = spot_position.loc[spot_position['symbol'] == 'USDT', '当前持仓量'].iloc[0]
                # 转资金到合约账户
                bn_transfer_spot_to_u(exchange, round(usdt - 1, 1))

        # ===发送信息推送
        send_wechat_work_msg(f'成功清仓下架币种，已将USDT转入U本位合约账户补充保证金', wechat_webhook_url)

        # ===休息一会儿
        time.sleep(2)


if __name__ == '__main__':
    try:
        timed_task()
    except KeyboardInterrupt:
        print('退出')
        exit()
    except BaseException as err:
        msg = '系统出错，10s之后重新运行，出错原因: ' + str(err)
        print(msg)
        send_wechat_work_msg(msg, error_webhook_url)
        traceback.print_exc()

